package it.unicam.cs.cosy.bioshape.gui.visualizer3d;

import hermesV3.util.HermesLogger;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.OutputStream;
import java.io.StringReader;
import java.util.*;

import javax.media.j3d.*;
import javax.vecmath.Matrix3d;
import javax.vecmath.Vector3d;

import com.sun.j3d.loaders.IncorrectFormatException;
import com.sun.j3d.loaders.ParsingErrorException;
import com.sun.j3d.loaders.Scene;
import com.sun.j3d.loaders.objectfile.ObjectFile;

/**
 * Updates the position of the {@code Entity} objects according to the
 * simulation log.
 * 
 * @author Federico Buti
 */
public class UpdateBehaviour extends Behavior {
    private WakeupCriterion timeOut;

    private HashMap<String, TransShape> shapes;

    private boolean play;

    Matrix3d m = new Matrix3d();

    Matrix3d m1 = new Matrix3d();

    Transform3D t = new Transform3D();

    Vector3d vect = new Vector3d();

    private File simuFile;

    Command nextCommand;

    private HashMap<String, Geometry[]> geometries;

    private BranchGroup mainBG;

    private BranchGroup workingBG;

    private int iteration;

    private ObjectInputStream simuFileObjectInputStream;

    private FileInputStream simuFileInputStream;

    UpdateBehaviour(File geomFile, BranchGroup mainBG, int timeDelay)
            throws IOException, ClassNotFoundException {
        this.timeOut = new WakeupOnElapsedTime(timeDelay);
        this.mainBG = mainBG;
        this.iteration = 0;
        // fill in this.geometries with any geometry description found in the
        // geom file
        // the key is the label
        // apparently for each label we have only a geometry, while in the
        // general
        // case there could be more geometries associated to the same label
        collectGeometries(geomFile);
        // .simu file for simulation output
        String filename = geomFile.getAbsolutePath();
        simuFile = new File(filename.substring(0, filename.indexOf(".g"))
                + ".simu");
        if (!simuFile.exists() || simuFile.isDirectory())
            throw new IllegalArgumentException("Simulation file '"
                    + filename.substring(0, filename.indexOf("."))
                    + ".simu' not "
                    + "found. Please review your configuration.");
        // COPY THE FILE --> avoid simulator conflicts
        // TODO Luca: ma serve?
        // File fileTemp = File.createTempFile("temp", "temp");
        // copy(simuFile, fileTemp);
        // simuFile = fileTemp;
        shapes = new HashMap<String, TransShape>();
        loadSimuFile();
    }

    void copy(File source, File target) throws IOException {

        InputStream in = new FileInputStream(source);
        OutputStream out = new FileOutputStream(target);

        // Copy the bits from instream to outstream
        byte[] buf = new byte[1024];
        int len;

        while ((len = in.read(buf)) > 0) {
            out.write(buf, 0, len);
        }

        in.close();
        out.close();
    }

    private void loadSimuFile() throws IOException, ClassNotFoundException {
        if (simuFileObjectInputStream != null) {
            mainBG.removeChild(workingBG);
            shapes.clear();
            simuFileInputStream.close();
            simuFileObjectInputStream.close();
        }
        workingBG = new BranchGroup();
        workingBG.setCapability(BranchGroup.ALLOW_DETACH);
        workingBG.setCapability(Group.ALLOW_CHILDREN_READ);
        workingBG.setCapability(Group.ALLOW_CHILDREN_WRITE);
        workingBG.setCapability(Group.ALLOW_CHILDREN_EXTEND);
        simuFileInputStream = new FileInputStream(simuFile);
        simuFileObjectInputStream = new ObjectInputStream(simuFileInputStream);
        // Extract from simuFile the Commands relative to this iteration, which
        // is the first one, == 0
        nextCommand = (Command) simuFileObjectInputStream.readObject();
        while (nextCommand.iteration == this.iteration) {
            // The Command will be of type CREATE, thus it contains the name of
            // the shape and the name of the geometry
            TransShape transShape = new TransShape(nextCommand.shapes[0],
                    geometries.get(nextCommand.shapes[1]));
            transShape.updatePosition(nextCommand.matrices[0]);
            shapes.put(transShape.name, transShape);
            workingBG.addChild(transShape.getHandle());
            nextCommand = (Command) simuFileObjectInputStream.readObject();
        }
        workingBG.compile();
        mainBG.addChild(workingBG);
    }

    /**
     * Reads the {@code Shape3D} object saved in the {@code .geom} file in
     * {@code Obj} format. Since the Java3D loader has problems loading multiple
     * {@code Obj} file stored in the same file (it loads correctly only the
     * first one read) the file is read and single string representation of the
     * saved {@code Obj} files are passed to the library so that all the shapes
     * are correctly loaded.
     * 
     * @param geomFile
     *            The {@code .geom} file containing the specification of the
     *            {@code Shape3D} objects.
     * @throws IOException
     *             Thrown if an I/O error occurs while reading the geometry
     *             file.
     * 
     * @see #readShape(String, String)
     */
    private void collectGeometries(File geomFile) throws IOException {
        geometries = new HashMap<String, Geometry[]>();
        BufferedReader br = new BufferedReader(new FileReader(geomFile));
        String line;
        String label = null;
        StringBuilder sb = new StringBuilder();
        while ((line = br.readLine()) != null) {
            if (line.length() == 0)
                continue;
            if (line.contains("# Simulation set-up data")
                    || (line.charAt(0) == 'g' && line.charAt(2) == ' ' && sb
                            .length() != 0)) {
                readShape(label, sb.toString());
                sb.delete(0, sb.length());
            }
            if (line.contains("# Simulation set-up data"))
                break;
            sb.append(line + "\n");
            if (line.charAt(0) == 'g' && line.charAt(2) == ' ')
                label = line.substring(3);
        }
        br.close();
    }

    /**
     * Reads the passed {@code Shape3D} and stores the information into the
     * {@link #geometries} data structure. The name associated to the shape in
     * the file is used in the data structure to index the shapes.
     * 
     * @param label
     *            The label that identifies the geometry in the .geom file. It
     *            is specified in Wavefront standard 3D object file format .obj
     * @param geometryDescription
     *            The shape to read.
     * @throws FileNotFoundException
     *             Never thrown: we pass a {@code String}.
     * @throws IncorrectFormatException
     *             Never thrown: we pass a {@code String}.
     * @throws ParsingErrorException
     *             Thrown if the loader encountered a problem parsing the passed
     *             {@code String}.
     */
    @SuppressWarnings("unchecked")
    private void readShape(String label, String geometryDescription)
            throws FileNotFoundException, IncorrectFormatException,
            ParsingErrorException {
        // Use ObjectFile loader code for the geometry
        ObjectFile objectFile = new ObjectFile(ObjectFile.TRIANGULATE
                | ObjectFile.STRIPIFY);
        Scene sceneFromGeometryDescription = objectFile.load(new StringReader(
                geometryDescription));
        Shape3D shape3DFromGeometryDescription = (Shape3D) sceneFromGeometryDescription
                .getNamedObjects().elements().nextElement();
        // Luca: usually only one associated geometry to a label
        ArrayList<Geometry> associatedListOfGeometries = Collections
                .list(shape3DFromGeometryDescription.getAllGeometries());
        geometries.put(label, associatedListOfGeometries
                .toArray(new Geometry[associatedListOfGeometries.size()]));
    }

    @Override
    public void initialize() {
        wakeupOn(timeOut);
    }

    @SuppressWarnings("rawtypes")
    @Override
    public void processStimulus(Enumeration criteria) {
        if (play) {
            iteration++;
            try {
                if (nextCommand == null)
                    nextCommand = (Command) simuFileObjectInputStream.readObject();
                do {
                    switch (nextCommand.type) {
                    case Command.CREATE:
                        TransShape ts = new TransShape(nextCommand.shapes[0],
                                this.geometries.get(nextCommand.shapes[1]));
                        ts.updatePosition(nextCommand.matrices[0]);
                        shapes.put(ts.name, ts);
                        workingBG.addChild(ts.getHandle());
                        break;
                    case Command.DESTROY:
                        // for (TransShape trs : shapes) {
                        // if (trs.name.equals(nextCommand.shapes[0])) {
                        // workingBG.removeChild(trs.bg);
                        // shapes.remove(trs);
                        // break;
                        // }
                        // }
                        break;
                    case Command.MOVE:
                        TransShape trs = shapes.get(nextCommand.shapes[0]);
                        trs.updatePosition(nextCommand.matrices[0]);
                        // for (TransShape trs : shapes)
                        // if (trs.name.equals(nextCommand.shapes[0])) {
                        // trs.updatePosition(nextCommand.matrices[0]);
                        // break;
                        // }
                    }
                } while ((nextCommand = (Command) simuFileObjectInputStream
                        .readObject()) != null && nextCommand.iteration == iteration);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (IOException e) {
                HermesLogger.log(HermesLogger.ERROR, getClass().getSimpleName()
                        + " - " + "error while reading the visualisation data."
                        + " Perhaps the simulation was not terminated or"
                        + " terminated not properly...");
                nextCommand = null; // to force reset if an error occurred
            }
            if (nextCommand == null)
                resetPositions(); // EOF|ERROR ---> restart!
        }
        wakeupOn(timeOut);
    }

    void setTimeOut(int timeDelay) {
        timeOut = new WakeupOnElapsedTime(timeDelay);
    }

    void setPlay(boolean play) {
        this.play = play;
    }

    boolean isPlay() {
        return play;
    }

    void resetPositions() {
        HermesLogger.log(HermesLogger.ERROR, getClass().getSimpleName() + " - "
                + "resetting visualisation...");
        try {
            iteration = 0;
            loadSimuFile();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

    /** Closes the file streams to ensure that the file is writable */
    void destroy() {
        try {
            if (simuFileInputStream != null)
                simuFileInputStream.close();
            if (simuFileObjectInputStream != null)
                simuFileObjectInputStream.close();
        } catch (IOException e) {
            HermesLogger.log(HermesLogger.ERROR, this.getClass()
                    .getSimpleName()
                    + " - error while closing the input file. "
                    + "Please restart the application to avoid loss of "
                    + "information");
        }
    }
}